# -*- coding: utf-8 -*-
import json
from datetime import datetime as dt, date
from decimal import Decimal
from sqlalchemy import Column
from sqlalchemy.types import Integer, DateTime, Text, Float, Boolean, Numeric, Unicode
from sqlalchemy.sql.expression import and_, desc
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import synonym

from flask import session, current_app as app

from sys2do.constant import ACTIVE, INACTIVE
from sys2do.model import qry, db, DBSession
from sys2do.util.decorator import trycatch
from sys2do.util.exception import ParamsNotOKExp


__all__ = ['SysMixin', 'DBMixin', 'AttributeDict', 'TreeMixin']

@trycatch( 1 )
def getUserID():
#    return None
    return session['user_profile']['id']

class AttributeDict( dict ):
    def __getattr__( self, attr ):
        return self[attr]
    def __setattr__( self, attr, value ):
        self[attr] = value

def getColumnMap( clz ):
    keycolumnMap = {}
    for i in clz.__table__.columns:
        keycolumnMap.update( {i.key: i} )
    return keycolumnMap

def getTypeMap( clz ):
    keytypeMap = {}
    for i in clz.__table__.columns:
        keytypeMap.update( {i.key: type( i.type )} )
    return keytypeMap


class SysMixin( object ):
    remark = Column( 'remark', Text, doc = u'备注' )

    createTime = Column( 'create_time', DateTime, default = None )
    updateTime = Column( 'update_time', DateTime, default = None )
    createById = Column( 'create_by_id', Integer, default = getUserID )
    updateById = Column( 'update_by_id', Integer, default = getUserID )

    sysCreateTime = Column( 'system_create_time', DateTime, default = dt.now )
    sysUpdateTime = Column( 'system_update_time', DateTime, default = dt.now, onupdate = dt.now )

    _attachment = Column( 'attachment', Text, doc = u'附件' )
    _comeFrom = Column( 'come_from', Text, doc = u'直接产生于' )
    active = Column( 'active', Integer, default = ACTIVE )    # 0 is active ,1 is inactive

    #===========================================================================
    # for enhancement use
    #===========================================================================

    textFd10 = Column( 'text_field_10', Text, default = None )
    textFd11 = Column( 'text_field_11', Text, default = None )
    textFd12 = Column( 'text_field_12', Text, default = None )
    textFd13 = Column( 'text_field_13', Text, default = None )
    textFd14 = Column( 'text_field_14', Text, default = None )

    intFd10 = Column( 'int_field_10', Integer, default = None )
    intFd11 = Column( 'int_field_11', Integer, default = None )
    intFd12 = Column( 'int_field_12', Integer, default = None )
    intFd13 = Column( 'int_field_13', Integer, default = None )
    intFd14 = Column( 'int_field_14', Integer, default = None )

    floatFd10 = Column( 'float_field_10', Float, default = None )
    floatFd11 = Column( 'float_field_11', Float, default = None )
    floatFd12 = Column( 'float_field_12', Float, default = None )
    floatFd13 = Column( 'float_field_13', Float, default = None )
    floatFd14 = Column( 'float_field_14', Float, default = None )

    @property
    @trycatch( None )
    def createBy( self ):
        from auth import User
        return qry( User ).get( self.createById )

    @property
    @trycatch( None )
    def updateBy( self ):
        from auth import User
        return qry( User ).get( self.updateById )

    @property
    @trycatch( None )
    def approveBy( self ):
        from auth import User
        return qry( User ).get( self.approveById )

    @trycatch( [] )
    def _getAttachment( self ):
        from system import SysFile
        ids = filter( bool, self._attachment.split( "|" ) )
        if not ids : return []
        return qry( SysFile ).filter( and_( SysFile.active == 0, SysFile.id.in_( ids ) ) ).order_by( SysFile.id )


    def _setAttachment( self, v ):
        ids = None
        if v :
            if type( v ) == list:
                ids = "|".join( map( unicode, v ) )
            elif isinstance( v, basestring ):
                ids = v
        self._attachment = ids


    @declared_attr
    def attachment( self ): return synonym( '_attachment', descriptor = property( self._getAttachment, self._setAttachment ) )


    @trycatch( None )
    def _getComeFrom( self ):
        if not self._comeFrom : return None
        import sys2do.model as m
        name, objid = self._comeFrom.split( "," )
        clz = getattr( m, name )
        obj = qry( clz ).get( objid )
        return obj or None


    def _setComeFrom( self, params ):
        if not isinstance( params, ( list, tuple ) ) : raise ParamsNotOKExp()
        if len( params ) != 2 : raise ParamsNotOKExp()
        ( clz, objid ) = params
        if not clz or not objid : raise ParamsNotOKExp()
        if not isinstance( clz, basestring ) :
            if hasattr( clz, "__name__" ) : clz = clz.__name__
            else : clz = clz.__class__.__name__
        self._comeFrom = ",".join( [ clz, unicode( objid ) ] )


    @declared_attr
    def comeFrom( self ):   return synonym( '_comeFrom', descriptor = property( self._getComeFrom, self._setComeFrom ) )


    @property
    def attributeDict( self ):
        return AttributeDict( self.__dict__ )

    def toJson( self, * args ):
        json_dict = {}
        if not args:
            args = self.__dict__.keys()
        for i in args:
            v = self.__dict__[i] if self.__dict__.has_key( i ) else getattr( self, i )
            if not i.startswith( '_' ):
                if isinstance( v, dt ):
                    json_dict[i] = v.strftime( '%Y-%m-%d %H:%M' )
                elif isinstance( v, Decimal ):
                    json_dict[i] = unicode( v )
                else:
                    json_dict[i] = v
        return json_dict


    # should change according to the special object
#    def creatable(self): return True
    def editable( self ): return True
    def deletable( self ): return True
    def approvable( self ): return True


    def viewLog( self ):
        from sys2do.model import SysLog, User
        return qry( SysLog, User ).filter( and_( SysLog.active == ACTIVE,
                                SysLog.refClz == self.__class__.__name__,
                                SysLog.refID == self.id,
                                User.id == SysLog.createById ) ).order_by( desc( SysLog.sysCreateTime ) )

    def viewURL( self ):  raise NotImplementedError


    def serialize( self, exclude_field = [] ):
        result = {}
        excluded = ['id', 'no', 'createTime', 'createById', 'updateTime',
                    'updateById', 'active', 'status', 'sysCreateTime', 'sysUpdateTime']

        if exclude_field:excluded.extend( exclude_field )

        m = self.__mapper__.columns
        for cname in m.keys():
            if cname.startswith( "_" ) : continue    # if it's start with "_" , it's not real fiels ,don't serialize
            colClz = m[cname]
            if isinstance( colClz, Column ) and cname not in excluded:
                v = getattr( self, cname )
                if v is None: v = ''
                elif isinstance( v, ( dt, date ) ): v = v.strftime( '%Y-%m-%d' )
                elif len( colClz.foreign_keys ) > 0 :
                    mclz = getattr( self, cname[:-2] ).__class__
                    v = unicode( DBSession.query( mclz ).get( v ) )
#                     print getattr( self, cname ), v
                result[cname] = ( v, colClz.doc or cname )
        return result








class DBMixin( object ):

    @classmethod
    def all( clz, conditions = [], order_by = None ):
        return clz.iall( conditions, order_by ).all()

    @classmethod
    def iall( clz, conditions = [], order_by = None ):
        if order_by is None: order_by = desc( clz.sysCreateTime )
        return qry( clz ).filter( and_( *conditions ) ).order_by( order_by )

    @classmethod
    def get( clz, id ):
        return qry( clz ).get( id )

    @classmethod
    def create( clz, params ):
        from sys2do.util.logic_helper import getCurrentUserID
        if 'createTime' not in params : params['createTime'] = dt.now()
        if 'updateTime' not in params : params['updateTime'] = dt.now()
        if 'createById' not in params : params['createById'] = getCurrentUserID()
        if 'updateById' not in params : params['updateById'] = getCurrentUserID()

        return clz( **params )

    def update( self, params ):
        for p in params: setattr( self, p, params[p] )

    @classmethod
    def getBy( cls, args = [], **kw ) :
        return cls.findBy( args, all = False, **kw ).first()

    @classmethod
    def findBy( clz, args = [], all = True, order_func = '', **kwargs ):
        '''
        keytypeMap = getTypeMap(clz)
        keycolumnMap = getColumnMap(clz)
        '''
        qyModel = qry( clz )
        for i in args:
            qyModel = qyModel.filter( i )
        '''
        for k, v in kwargs.iteritems():
            _column = keycolumnMap[k]
            if k in keytypeMap.keys():
                _type = keytypeMap[k]
                if type(v) in (list, tuple):
                    qyModel = qyModel.filter(getattr(clz, k)in(v))
                else:
                    if _type == Integer:
                        if v or v==0:
                            qyModel = qyModel.filter(getattr(clz, k)==int(v))
                    elif _type == Boolean:
                        if v or v==False:
                            qyModel = qyModel.filter(getattr(clz, k)==v)
                    elif v:
                        qyModel = qyModel.filter(getattr(clz, k).ilike('%%%s%%' % v))
        '''
        if order_func:
            qyModel = qyModel.order_by( order_func )
        return qyModel.all() if all else qyModel

    @classmethod
    def findByIDs( clz, ids, separator = ',', order_func = 'id asc' ):
        if type( ids ) == str or type( ids ) == unicode:
            results = []
            id_list = filter( lambda x: x, ids.split( separator ) )
            result_dict = {}
            for result in qry( clz ).filter( clz.id.in_( id_list ) ).order_by( order_func ).all():
                result_dict[result.id] = result
            for id in ids.split( separator ):
                results.append( result_dict[int( id )] if id else None )
            return results
        elif type( ids ) == list or type( ids ) == tuple:
            return qry( clz ).filter( clz.id.in_( ids ) ).order_by( order_func ).all()

    @classmethod
    def initNew( clz, *keys, **kw ):
        if kw.get( 'id', None ):
            del kw['id']
        return clz.init( *keys, **kw )

    @classmethod
    def init( clz, *keys, **kw ):
        try:
            obj = clz( **clz._resetKw( *keys, **kw ) )
            return obj
        except Exception, e:
            app.logger.exception( str( e ) )
            raise e

    @classmethod
    def saveNew( clz, *keys, **kw ):
        try:
            obj = clz.init( *keys, **kw )
            from sys2do.util.logic_helper import getCurrentUserID
            if not obj.createTime: obj.createTime = dt.now()
            if not obj.updateTime: obj.updateTime = dt.now()
            db.add( obj )
            db.flush()
            return obj
        except Exception, e:
            db.rollback()
            app.logger.exception( str( e ) )
            raise e

    def saveEdit( self, *keys, **kw ):
        try:
            new_params = self.__class__._resetKw( *keys, **kw )
            old_params = self.__dict__
            for k, v in new_params.iteritems():
                if not old_params.get( k, None ) == v:
                    setattr( self, k, v )
            # db.flush()
            return self
        except Exception, e:
            db.rollback()
            app.logger.exception( str( e ) )
            raise e

    @classmethod
    def _resetKw( clz, *keys, **kw ):
        # keytypeMap = getTypeMap(clz)
        params = {}
        from sqlalchemy.orm.attributes import InstrumentedAttribute
        keys = []
        for v in clz.__dict__.values():
            if type( v ) == InstrumentedAttribute:
                keys.append( v.key )
        for k, v in kw.iteritems():
            if k in keys:
                params[k] = v

        '''
        for k,v in kw.iteritems():
            _column = getColumnMap(clz)[k]
            if not k.startswith('_') and k in keytypeMap.keys():
                if type(v) in (list, tuple):
                    params[k] = ','.join(map(str, v))
                elif type(v) == dict:
                    params[k] = json.dumps(v)
                elif k.endswith('_id') or k=='id':
                    params[k] = int(v) if v else None
                else:
                    if (keytypeMap[k] in (Integer, Numeric)) and not v:
                        params[k] = None
                    elif keytypeMap[k]==Boolean:
                        params[k] = True if v in ACTIVE_TRUE_LIST else False
                    elif keytypeMap[k]==DateTime and not v:
                        params[k] = None
                    else:
                        params[k] = v
        '''
        return params

class ProductMixin( object ):

    pdtno = Column( 'pdt_no', Text )    # 珠宝系统编号
    pdtbarcode = Column( 'pdt_barcode', Text )    # 珠宝条码
    pdtname = Column( 'pdt_name', Text )    # 珠宝名字
    pdttype = Column( 'pdt_type', Text )    # 珠宝类型
    pdtseries = Column( 'pdt_series', Text )    # 系列
    pdtshap = Column( 'pdt_shap', Text )    # 形状

    pdtstyleNo = Column( 'pdt_style_no', Text )    # 款号
    pdtweight = Column( 'pdt_weight', Text )    # 重量
    pdtlength = Column( 'pdt_length', Text )    # 手寸长度
    pdtclarity = Column( 'pdt_clarity', Text )    # 净度
    pdtcolor = Column( 'pdt_color', Text )    # 颜色
    pdtgoldFineness = Column( 'pdt_gold_fineness', Text )    # 金成色
    pdtcertificate = Column( 'pdt_certificate', Text )    # 证书号

    pdtlength = Column( 'pdt_length', Text, )    # 长度
    pdtwidth = Column( 'pdt_width', Text, )    # 宽度
    pdtheight = Column( 'pdt_height', Text, )    # 高度

    pdtpPrice = Column( 'pdt_purchase_price', Float )    # 进货价
    pdtsPrice = Column( 'pdt_sale_price', Float )    # 卖货价
    pdtoldsPrice = Column( 'pdt_old_sale_price', Float )    # 旧的卖货价
    pdtfee = Column( 'pdt_fee', Float, default = 0 )    # 手工费
    pdtdesc = Column( 'pdt_desc', Text )


    pdtfulldesc = Column( 'pdt_full_desc', Text )

    def copyPdtInfo( self, pdt ):
        for col in ['no', 'barcode', 'name', 'styleNo', 'weight', 'length', 'clarity',
                    'color', 'goldFineness', 'certificate', 'pPrice', 'sPrice', 'oldsPrice',
                    'fee', 'desc', 'length', 'width', 'height']:
            setattr( self, 'pdt%s' % col, getattr( pdt, col ) )
        for ( fid, f ) in [( 'typeID', 'type' ), ( 'seriesID', 'series' ),
                        ( 'shapID', 'shap' ), ]:

            v = unicode( getattr( pdt, f ) ) if getattr( pdt, fid ) else None
            setattr( self, 'pdt%s' % f, v )

        setattr( self, 'pdtfulldesc', pdt.makeDesc() )


class TreeMixin( object ):

    __tablename__ = None

    name = Column( Unicode( 100 ) )
    lft = Column( Integer )
    rgt = Column( Integer )
    # level = Column(Integer)
    # isLeaf = Column( 'is_leaf', Boolean )

    parentID = Column( 'parent_id', Integer )

    @property
    def parent( self ):
        return qty( self ).get( self.parentID ) if self.parentID else None

    def getTree( self, args = [], includeSelf = False ):
        clz = self.__class__
        if includeSelf:
            return clz.findTree( [clz.lft >= self.lft, clz.rgt <= self.rgt] )
        else:
            return clz.findTree( [clz.lft > self.lft, clz.rgt < self.rgt] )

    @classmethod
    def findTree( clz, args = [] ):
        qyModel = qry( clz )
        for i in args:
            qyModel = qyModel.filter( i )
        return qyModel.order_by( clz.lft ).all()

    def setLeaf( self, **kw ):
        try:
            tbName = self.__tablename__
            if not self.parentID or int( self.parentID ) == 0:
                max_rgt = db.query( func.max( self.rgt ) ).filter( self.parentID == None ).one()[0]
                max_rgt = max_rgt if max_rgt else 0
                self.lft = max_rgt + 1
                self.rgt = max_rgt + 2
            else:
                parent = self.parent
                con = ''
                for k, v in kw.iteritems():
                    con += ( ' and %s=%s' % ( k, v ) )
                db.execute( 'update %s set lft=lft+2 where active=%s and lft>%s%s' % ( tbName, ACTIVE, parent.rgt, con ) )
                db.execute( 'update %s set rgt=rgt+2 where active=%s and rgt>=%s%s' % ( tbName, ACTIVE, parent.rgt, con ) )
                self.lft = parent.rgt
                self.rgt = parent.rgt + 1
            return self
        except Exception, e:
            db.rollback()
            app.logger.exception( str( e ) )
            raise

    def moveLeaf( self, newParentID, **kw ):
        try:
            tbName = self.__tablename__
            con = ''
            for k, v in kw.iteritems():
                con += ( ' and %s=%s' % ( k, v ) )

            activeTemp = -1
            # set current tree to temp active status and reset lft start from 1
            db.execute( 'update %s set active=%s, lft=lft-%s, rgt=rgt-%s where (lft between %s and %s)%s' % ( tbName, activeTemp, self.lft - 1, self.lft - 1, self.lft, self.rgt, con ) )
            # reset old parent tree
            width = self.rgt - self.lft + 1
            db.execute( 'update %s set lft=lft-%s where active=%s and lft>%s%s' % ( tbName, width, ACTIVE, self.rgt, con ) )
            db.execute( 'update %s set rgt=rgt-%s where active=%s and rgt>%s%s' % ( tbName, width, ACTIVE, self.rgt, con ) )
            # reset new parent tree
            newParent = self.get( newParentID )
            db.execute( 'update %s set lft=lft+%s where active=%s and lft>%s%s' % ( tbName, width, ACTIVE, newParent.rgt, con ) )
            db.execute( 'update %s set rgt=rgt+%s where active=%s and rgt>=%s%s' % ( tbName, width, ACTIVE, newParent.rgt, con ) )
            # reset current tree to new tree and reset lft from new parent lft+1
            minus = newParent.rgt - 1
            db.execute( 'update %s set lft=lft+%s, rgt=rgt+%s, active=%s where active=%s' % ( tbName, minus, minus, ACTIVE, activeTemp ) )
        except Exception, e:
            db.rollback()
            app.logger.exception( str( e ) )
            raise e

    def delLeaf( self, **kw ):
        try:
            tbName = self.__tablename__
            con = ''
            for k, v in kw.iteritems():
                con += ( ' and %s=%s' % ( k, v ) )
            db.execute( 'update %s set active=%s, lft=none, rgt=none where (lft between %s and %s)%s' % ( tbName, INACTIVE, self.lft, self.rgt, con ) )
            # reset old parent tree
            width = self.rgt - self.lft + 1
            db.execute( 'update %s set lft=lft-%s where active=%s and lft>%s%s' % ( tbName, width, ACTIVE, self.rgt, con ) )
            db.execute( 'update %s set rgt=rgt-%s where active=%s and rgt>%s%s' % ( tbName, width, ACTIVE, self.rgt, con ) )
        except Exception, e:
            db.rollback()
            app.logger.exception( str( e ) )
            raise e
